リファクタリング 既存のコードを安全に改善する (第 2 版)
https://images-na.ssl-images-amazon.com/images/I/41+4LYE83XL._SX383_BO1,204,203,200_.jpg
概要
対象読者 : 普段からソフトウェア開発をしている職業プログラマ
コード例としては JavaScript を利用 (とはいえ、JavaScript 以外の言語にも適用可能) 前半 4 章でリファクタリングとは何かや、リファクタリングの思想が語られる
感想
全体として、リファクタリングの入門書として良い内容だが、リファクタリングカタログはちょっと冗長かも
前半のリファクタリングの説明や思想の説明は、ソフトウェア開発を行う人全員におさえておいてほしい内容だと感じた
リファクタリング初心者の人には前半だけでも読んでもらいたい
リファクタリングカタログについては、一覧を丸暗記するような使い方はしなくてよくて、カタログの中で説明される設計原則や考え方を学んでいくと良さそう
いずれも当たり前に使いこなして欲しいリファクタリングが書かれている
という点で悪い内容ではないが、レベル感としてはかなり初心者向きという印象
リファクタリングについて一定の経験がある人にとっては当たり前のことだらけ
「なぜこういうリファクタリングをすべきなのか」 という点を、原則なども紹介しながら説明してくれる点は (設計のベストプラクティスを学んでいる途中の人にとって) 有益だと感じた
実際のリファクタリング手順はちょっと冗長かも? 初心者が見て学ぶには良いが、読み進めるのは大変かもしれない
自分はさらっとだけ読んでいった
内容メモ
本書の構成
1 章 : 例
2 章 : リファクタリングの理論 (一般原則、定義、使うべき理由)
3 章 : におうコードの見分け方と、リファクタリングで一掃する方法
4 章 : コード中にテストを組み込む方法
5 章以降 : リファクタリングのカタログ
1 章 : リファクタリング - 最初の例
構造的に新機能を追加しづらいプログラムに新機能を追加する必要がある場合、まずは機能追加が簡単になるようにリファクタリングすべし
プログラムに構造が欠けているなら、たいていの場合、まずは構造を足していく方が簡単
汚いコードでも、コードが正常に動作していて変更の予定もないなら放置で良い
コードの動作を理解して、流れを追わなければならなくなったならば、行動 (リファクタリング) をおこす必要がある
リファクタリングを行うとき、最初にすることは対象となるコードにきちんとしたテスト群を作ること
テストは不可欠
リファクタリングの中心となるプロセス : 小さな変更と変更のたびのテスト
多くの人がリファクタリングの優先度を低くしているが、バランスの問題 : コードベースは最初に見た時よりもきれいに保つように
リファクタリングの通常の流れ
コードを読む
なんらかの洞察を得る
リファクタリングを適用し、頭の中にある洞察をコードで表現する
良いコードかどうかは、変更がどれだけ容易かで決まる
2 章 : リファクタリングの原則
2 つの帽子 : 機能追加とリファクタリングを区別する (機能追加するときにはリファクタリングしない、その逆も然り)
いつリファクタリングする?
機能追加を容易にするための準備
コードをわかりやすくするための理解のためのリファクタリング
頭の中にある理解をコードに移し替えていく
派生として、ゴミ拾いのリファクタリング : コードが何をしているかわかるが書き方がいまいち、というとき
あらかじめリファクタリングの時間を確保するのではなく、機能の追加やバグ修正の中でリファクタリングする
協調してまとまった時間をとってリファクタリングする必要が出てくることもあるかもしれないが、まれ
リファクタリングと機能追加をバージョン管理システム上で別コミットにすることについて
それぞれ独立してレビューできるため、おすすめとの意見
筆者はあまり納得してないらしい (リファクタリングと機能追加は密接に結びついているからわざわざ分ける必要はないし、リファクタリングのコンテキストが失われてしまう、という意見)
nobuoka.icon 絶対分けた方がレビューしやすくていいと思うけどなー
リファクタリングを避けるべきときもある
変更しないし理解する必要もないコードをリファクタリングする意味はない
ゼロから作り直した方が速い場合もある (ただしその判断は非常に難しい)
リファクタリングするかどうかは常に経済的な観点で考えること
新機能の追加やバグ修正などの速度向上のためにやる
「美しいコード」 「素晴らしいエンジニアリングのためのプラクティス」 のような道徳的理由でリファクタリングしないこと
リファクタリングを実施すると、意味的な変更によるマージの問題が発生しやすい
別の解 : すぐれた自動リファクタリング機能をサポートする開発環境を使っているならテストなしでも進める
が、ここでも問題はテストがないこと → 解決策はテストを追加すること
第 1 版では、データベースのリファクタリングは問題の多い分野と書いたが、その後状況が変わった
通常のリファクタリングと違い、正式に至るまでに複数のリリースに分割すべし (本番運用で問題が起きても取り消すことが容易になる)
リファクタリングが設計に与えた影響
「将来もしかしたら必要になるかも」 という設計ではなく、いま必要な設計をする (将来必要になればその時点でリファクタリングできる)
この設計方針には様々な名前が付けられている : シンプルな設計、インクリメンタル設計、Yagni 3 章 : コードの不吉な臭い
いつリファクタリングを開始して、いつ終えるのか?
4 章 : テストの構築
テストはプログラミングを始める前に手を付けるのが良い
それにより、実装よりもインターフェイスに集中できる
「全ての公開メソッドをテストすべし」 と言う人もいるが、著者はそうは思っていない
テストはリスク主導であるべき : バグが起こりえないようなところにテストを書く必要はない
多くのテストを書こうとするあまり、必要なテストを漏らしては意味がない
一番怪しいところを集中的にテストする
どれだけテストすれば十分か? → 適切な指標はない
5 章 : カタログの紹介
以降はリファクタリングのカタログ
カタログは次の形式で表現
名前
スケッチ : リファクタリング前後でどのようにコードが変わるかを示す
動機 : リファクタリングをすべき理由と、避けるべき状況
手順 : リファクタリングの実施手順
例 : リファクタリングの使用例
6 章 : リファクタリングはじめの一歩
関数の抽出 / 関数のインライン化 / 変数の抽出 / 変数のインライン化
処理や変数を抽出するのは、結局のところ名前をつけるため
意図と実装の分離 (そのコードが何をしているか調べないとわからないコード片があるなら、その 「何」 に名前をつける)
関数宣言の変更 : 関数宣言はソフトウェアシステムの継ぎ目であり、わかりやすくするべし
変数のカプセル化 : 広い範囲で利用されるデータを移動したいときは、カプセル化して変数へのアクセスを関数経由にする
変数名の変更 : 変数名をわかりやすくする
パラメータオブジェクトの導入 : 関連のある値をまとめる
関数群のクラスへの集約 : 関連のある処理をまとめる
関数群の変換への集約 : 複数の関数を 1 つの関数にまとめるぽい
フェーズの分離 : 一連の処理の中に異なる目的の操作が入り乱れている場合 (データの変換と出力が入り乱れてる、とか) に、分離する
7 章 : カプセル化
レコードのカプセル化 : 構造体ではなくクラスを使う
コレクションのカプセル化 : クラスが持つコレクションなどを、外部から直接操作させないようにする
オブジェクトによるプリミティブの置き換え : 単純な表示以上のことをするなら、ちょっとしたデータでも新たなクラスにする
問い合わせによる一時変数の置き換え : 変数を関数に置き換えることで、関数の抽出が容易になる
クラスの抽出 / クラスのインライン化 : たくさんのデータやメソッドを持つ巨大なクラスは切り出せるところを切り出そう
委譲の隠蔽/ 仲介人の除去 : クラスが持つ別オブジェクトに委譲する必要がある処理をメソッドに隠蔽したりする
アルゴリズムの置き換え : 複雑なアルゴリズムを簡単なアルゴリズムで書き換えられることがわかったら書き換える
8 章 : 特性の移動
関数の移動 : 関数を移動させる
すぐれたソフトウェア設計の核心はモジュール性。 どこに何を置くのかが重要であり、移動させていく必要もある
フィールドの移動 : プログラムの力の源はデータ構造なので、正しくないと分かったデータ構造は変更すべき
ステートメントの関数内への移動 : 関数呼び出し側で常に同じ処理をするなら関数内に移動する
ステートメントの呼び出し側への移動 : 上の逆
関数呼び出しによるインラインコードの置き換え : nobuoka.icon 関数の抽出とは違うのか??
ステートメントのスライド : ステートメントの位置を入れ替えて、関連する処理をまとめよう、という感じ
ループの分離 : 1 つのループの中で複数の処理をやっているなら、それぞれの処理のためのループに分割する
パイプラインによるループの置き換え : パイプライン処理で書くと処理を追いやすい (そうか? nobuoka.icon)
デッドコードの削除 : デッドコードは邪魔
9 章 : データの再編成
変数の分離 : ひとつの変数が複数の役割に使われているなら、別の変数として定義しよう
フィールド名の変更 : フィールド名を変更したいときにどう進めればよいか
問い合わせによる導出変数の置き換え : 簡単に計算できる値は変数に保持するのではなく、毎回計算する
値から参照への変更 : 同一のものをあらわす複数の値オブジェクトを使うのではなく、データを保持するオブジェクトは 1 つにしてそれを参照する
10 章 : 条件記述の単純化
条件記述の分解 : ややこしい条件をべた書きするのではなく、関数にして抽象化する (「関数の抽出」 の特殊ケース)
条件記述の統合 : 異なる判定条件で同じ処理をしているなら、それらの条件を判定する 1 つの関数に統合できる可能性
ガード節による入れ子の条件記述の置き換え : 例外的な場合はいわゆる早期リターンを使って例外状況であることを表現
ポリモーフィズムによる条件記述の置き換え
アサーションの導入 : 事前条件を表現するためにアサーションを利用する 11 章 : API のリファクタリング
パラメータによる関数の統合 : 関数内のリテラルが違うだけの似た関数は、パラメータを受け取る 1 つの関数にできる
フラグパラメータの削除 : 関数内の処理を切り替えるためにパラメータを受け取る関数は、複数の関数に分ける
オブジェクトそのものの受け渡し : nobuoka.icon 「パラメータオブジェクトの導入」 と大した違いはない気がする
問い合わせによるパラメータの置き換え : パラメータで受け取らなくていい値 (別のパラメータから計算できるとか) を受け取っているなら、それは削除してよい
パラメータによる問い合わせの置き換え : 関数内部で関数外部のオブジェクトを参照するなら、それをパラメータで受け取る
setter の削除 : 状態を変更する必要がないオブジェクトについては、状態を変更できないようにすべし
ファクトリ関数によるコンストラクタの置き換え : コンストラクタには制約があるので、オブジェクト生成用の関数を用意
コマンドによる関数の置き換え : 関数呼び出し (関数に渡すパラメータを含める) をオブジェクト化 (コマンドオブジェクト) して柔軟性を高める undo などを実装しやすくなる
関数によるコマンドの置き換え : コマンドオブジェクトの利用にはコストが伴うので、必要ないなら関数呼び出しにする 12 章 : 継承の取り扱い
クラスの特性を継承階層の上下に移動させる
メソッドの引き上げ / フィールドの引き上げ / コンストラクタ本体の引き上げ : サブクラスで共通の特性をスーパークラスへ
メソッドの押し下げ / フィールドの押し下げ : 1 つまたは少数のサブクラスのための特性はスーパークラスからサブクラスへ
サブクラスによるタイプコードの置き換え : オブジェクトを分類する際にタイプコードを使うだけでなく型付けする
特定の種類の場合にのみ必要なフィールドがあるとか、種類ごとに処理が異なる場合 (ポリモーフィズムで対応)
直接サブタイプにするか、タイプコードにクラス階層をもたらすかのどちらか
継承階層にクラスを追加
サブクラスの削除 : 存在価値がなくなったサブクラスは削除
スーパークラスの抽出 : 複数のクラスがほぼ同じ処理を行っているなら、スーパークラスで共通化
nobuoka.icon 委譲でいい場合も多そう
クラス階層の平坦化 : スーパークラスとサブクラスをまとめる
nobuoka.icon サブクラスの削除とほぼ同じでは??
委譲によるサブクラスの置き換え : クラス階層は 1 軸のみでしかできないのと、親と子の結合が密になる問題を委譲で対処
「クラス継承よりもオブジェクトのコンポジションを優先せよ」 というよく知られた原則 「inheritance considered harmful」 と解釈する人が多いが、筆者は継承を絶対使わない方が良いと思っているわけではない
State パターンまたは Strategy パターンで置き換えるイメージ
ポリモーフィズム的な処理の切り替えをサブクラスでやるのではなく、委譲先オブジェクトの切り替えで実装
委譲によるスーパークラスの置き換え : 複数のサブクラスで使われるスーパークラスの処理を別クラスに切り出してそこに委譲